module ScriptLoader = "ScriptLoader.js";

import Promise from "Promise.js";

var SCHEME_RE = /^[a-z]+:/i;

var HOP = {}.hasOwnProperty,
    globalObject = this,
    home = dirname(globalObject.location.pathname),
    modules = {},
    globalNames = {},
    urlMap = {},
    initialized = false,
    pending = 0,
    pendingList = [],
    noCache = false;

// Loads a module and returns a promise for the module object
export function load(url) {

    initialize();
    return loadModule(url, home).then(m => m.deref());
}

// Configures the module loader
export function configure(options) {

    if (!initialize(options))
        throw new Error("Loader has already been initialized.");
}

// Defines a module
export function define(url, exports) {

    url = resolve(url, home);
    
    if (hasKey(modules, url))
        throw new Error("Module '" + url + "' is already loaded.");
    
    modules[url] = {
    
        future: Promise.when(M),
        deref() { return exports; }
    };
}

// The global module registration function
function __MODULE(fn, dep) {

    ScriptLoader.getContext(context => {
    
        if (!context.factory) {
        
            context.factory = fn;
            context.dependencies = dep;
        }
    });
}

// Loads a script and returns a promise for completion
function loadScript(url) {

    var src = url + (noCache ? ("?" + new Date().getTime()) : "");
    
    globalObject.__MODULE = __MODULE;
    
    return ScriptLoader.loadScript(src);
}
    
// Initializes the global environment
function initialize(config) {

    if (initialized)
        return false;
    
    config = config || {};
        
    if (typeof config.mappings === "object" && config.mappings)
        urlMap = config.mappings;
    
    if (config.overrideCache === true)
        noCache = true;
    
    return initialized = true;
}

// Loads a module
function loadModule(url, dir) {

    var M, promise;
    
    url = resolve(url, dir);
    dir = dirname(url);
    
    // Return promise if already loading
    if (hasKey(modules, url))
        return modules[url].future;
    
    promise = new Promise;
    
    M = modules[url] = {
    
        future: promise.future,
        exports: null,
        factory: null,
        
        deref() {
    
            if (this.exports)
                return this.exports;
                
            if (!this.factory)
                throw new Error("Module '" + url + "' could not be loaded.");
            
            this.factory.call(globalObject, 
                url => modules[resolve(url, dir)].deref(), 
                this.exports = {});
        
            return this.exports;
        }
    };
    
    pending += 1;
    pendingList.push(() => resolvePromise());

    loadScript(url).then(value => {
    
        var deps = [];
        
        if (value && value.factory) {
        
            var fn = value.factory,
                req = value.dependencies || [];
            
            M.factory = fn;
            
            // Load dependencies
            while (req.length)
                deps.push(loadModule(req.shift(), dir));
        
        } else if (hasKey(globalNames, url)) {
        
            M.exports = globalObject[globalNames[url]] || null;
        }
        
        // If everything requested has been fetched, resolve all
        // promises (will break any cycles that exist).
        if (--pending === 0)
            while (pendingList.length > 0)
                pendingList.pop()();
        
        Promise.whenAll(deps).then(
            val => resolvePromise(), 
            err => rejectPromise(err));
    });
    
    return M.future;
    
    function resolvePromise() {
    
        if (promise) {
        
            promise.resolve(M);
            promise = null;
        }
    }
    
    function rejectPromise(err) {
    
        if (promise) {
        
            promise.reject(M);
            promise = null;
        }
    }
}


// Resolves a module reference
function resolve(url, dir) {

    return normalize(mapURL(url), dir);
}

// Performs module namespace mappings
function mapURL(url) {

    var entry;
    
    if (hasKey(urlMap, url)) {
    
        entry = urlMap[url];
        
        if (typeof entry === "string")
            entry = { url: entry };
        
        // Store global name for loading non-modules
        if (entry.globalName)
            globalNames[normalize(entry.url || url, home)] = entry.globalName;
                
        if (entry.url && entry.url !== url) {
        
            url = mapURL(entry.url);
            
            if (url === entry.url)
                url = normalize(url, home);
        }
    }
    
    return url;
}

// Returns true if the object has the specified property
function hasKey(o, k) { return HOP.call(o, k); }

// Normalized a URL relative to a directory
function normalize(url, dir) {

    if (!SCHEME_RE.test(url)) {
    
        if (url.charAt(0) !== "/")
            url = dir + "/" + url;
        
        url = collapse(url);
    }
    
    return url;
}

// Gets the directory from a path
function dirname(path) {

    var i = path.lastIndexOf("/");
    return (i >= 0) ? path.slice(0, i) : "";
}

// Removes relative segments from a path
function collapse(path) {

    var a = path.split("/"), out = [], i, e;
    
    for (i = 0; i < a.length; ++i) {
    
        e = a[i];
        
        if (e === ".") continue;
        else if (e === ".." && out.length > 0) out.pop();
        else out.push(e);
    }
    
    return out.join("/");
}
